Критическая секция (critical section) — это участок кода, требующий монопольного доступа к каким-то общим данным (ресурсам); параллельное выполнение этого участка кода несколькими потоками может привести к непредсказуемым или неверным результатам.
Понятие критической секции подразумевает, что единовременно только один поток получал доступ к определенному ресурсу (участку кода, манипулирующему с данным ресурсом). Система может в любой момент вытеснить поток, находящийся в критической, но ни один из потоков, которым нужен занятый ресурс, не получит процессорное время до тех пор, пока владеющим им поток не выйдет за границы критической секции.
Название критическая секция используется и для объекта синхронизации – структуры (типа данных) Windows, используемой для организации критической секции в изложенном выше смысле.
Для реализации взаимного исключения с помощью критической секции необходимо глобально (обеспечить доступность из всех потоков процесса, конкурирующих за ресурс) объявить переменную типа CRITICAL_SECTION. Структура CRITICAL_SECTION представляет для программиста «черный ящик», то есть не происходит прямого обращения к полям данной структуры. Работа с CRITICAL_SECTION осуществляется исключительно через специальные функции, которым передается адрес соответствующего экземпляра данной структуры.
Перед началом использования (до обращения какого-либо потока к защищенному ресурсу) элементы структуры необходимо инициализировать с помощью вызова:
void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
Захват критической секции производится с помощью функций:
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
BOOL TryEnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
При вызове EnterCriticalSection происходит анализ элементов структуры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. EnterCriticalSection выполняет следующие действия.
Если ресурс свободен, EnterCriticalSection модифицирует элементы структуры, указывая, что вызывающий поток занимает ресурс, после чего немедленно возвращает управление, и поток продолжает свою работу (получив доступ к ресурсу).
Если значения элементов структуры свидетельствуют, что ресурс уже захвачен вызывающим потоком, EnterCriticalSection обновляет их, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и возвращает управление потоку. Таким образом, поток, владеющий критической секцией, может повторно войти в критическую секцию, что дает возможность организации рекурсивных функций.
Если значения элементов структуры указывают на то, что ресурс занят другим потоком, EnterCriticalSection переводит вызывающий поток в режим ожидания. Система запоминает, что данный поток хочет получить доступ к ресурсу, и — как только поток, занимавший критическую секцию, освобождает ее, ожидающий поток переходит в активное состояние и занимает критическую секцию. Интересным фактом является то, что потоки, ожидающие освобождения критической секции, на самом деле не блокируются «навечно». Данная функция реализована таким образом, что по истечении определенного времени, генерирует исключение (ожидание прекращается ошибкой). Длительность времени ожидания функцией EnterCriticalSection определяется значением системного реестра Windows. Длительность времени ожидания измеряется в секундах и по умолчанию равна 2 592 000 секунд (что составляет ровно 30 суток).
Использование функции TryEnterCriticalSection позволяет опросить критическую секцию, для проверки занята она другим потоком или нет. Возвращение данной функцией значения True свидетельствует о том, что вызывающий поток приобрел права владения критическим участком кода, тогда как значение False говорит о том, что данный критический участок кода уже принадлежит другому потоку. Функция TryEnterCriticalSection никогда не приостанавливает выполнение вызывающего потока, позволяя проверить доступность ресурса и в случае его занятости обработать такую ситуацию соответствующим образом.
В конце участка кода, использующего разделяемый ресурс, должен присутствовать
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
Эта функция просматривает элементы структуры CRITICAL_SECTION и уменьшает счетчик числа захватов ресурса вызывающим потоком на 1. Если его значение больше 0, LeaveCriticalSection ничего не делает и просто возвращает управление. Если значение счетчика достигло 0, LeaveCriticalSection сначала выясняет, есть ли в системе другие потоки, ждущие данный ресурс в вызове EnterCriticalSection. Если есть хотя бы один такой поток, функция настраивает значения элементов структуры, чтобы они сигнализировали о занятости ресурса, и отдает его одному из ожидающих потоков при присутствии таковых. LeaveCriticalSection никогда не приостанавливает поток, а управление возвращает немедленно.
При завершении работы с критической секцией для нее необходимо вызвать функцию
void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
Данная функция сбрасывает все переменные-члены внутри структуры CRITICAL_SECTION. Естественно, нельзя удалять критическую секцию в тот момент, когда ею все еще пользуется какой-либо поток.
В силу своей простоты и преимуществ в отношении производительности критические секции являются предпочтительным механизмом синхронизации, если из возможностей достаточно для того, чтобы удовлетворить требования, предъявляемые решаемой задачей.
Однако критическая секция имеет один существенный недостаток: она не может быть использована для синхронизации потоков принадлежащих различным процессам.
Не нашли то, что искали? Воспользуйтесь поиском:
Лучшие изречения: Для студента самое главное не сдать экзамен, а вовремя вспомнить про него. 10553 — | 7758 — или читать все.
Важным понятием синхронизации потоков является понятие «критической секции» программы. Критическая секция — это часть программы, результат выполнения которой может непредсказуемо меняться, если переменные, относящиеся к .этой части программы, изменяются другими потоками в то время, когда выполнение этой части еще не завершено. Критическая секция всегда определяется по отношению к определенным критическим данным, при несогласованном изменении которых могут возникнуть нежелательные эффекты. В предыдущем примере такими критическими данными являлись записи файла базы данных. Во всех потоках, работающих с критическими данными, должна быть определена критическая секция. Заметим, что в разных потоках критическая секция состоит в общем случае из разных последовательностей команд.
Чтобы исключить эффект гонок по отношению к критическим данным, необходимо обеспечить, чтобы в каждый момент времени в критической секции, связанной с этими данными, находился только один поток. При этом неважно, находится этот поток в активном или в приостановленном состоянии. Этот прием называют взаимным исключением. Операционная система использует разные способы реализации взаимного исключения. Некоторые способы пригодны для взаимного исключения при вхождении в критическую секцию только потоков одного процесса, в то время как другие могут обеспечить взаимное исключение и для потоков разных процессов.
Самый простой и в то же время самый неэффективный способ обеспечения Взаимного исключения состоит в том, что операционная система позволяет потоку запрещать любые прерывания на время его нахождения в критической секции. Однако этот способ практически не применяется, так как опасно доверять управление системой пользовательскому потоку — он может надолго занять процессор, а при крахе потока в критической секции крах потерпит вся система, потому что прерывания никогда не будут разрешены.
Для синхронизации потоков одного процесса прикладной программист может использовать глобальные блокирующие переменные. С этими переменными, к которым все потоки процесса имеют прямой доступ, программист работает, не обращаясь к системным вызовам ОС.
Рис. 4.4.Реализация критических секций с использованием блокирующие переменных
Каждому набору критических данных ставится в соответствие двоичная переменная, которой поток присваивает значение 0, когда он входит в критическую секцию, и значение 1, когда он ее покидает. На рис. 4.4. показан фрагмент алгоритма потока, использующего для реализации взаимного исключения доступа к критическим данным 0 блокирующую переменную F(D). Перед входом в критическую секцию поток проверяет, не работает ли уже какой-нибудь поток с данными D. Если переменная F(D) установлена в 0, то данные заняты и проверка циклически повторяется. Если же данные свободны (F(D) =1), то значение переменной F(D) устанавливается в 0 и поток входит в критическую секцию. После того как поток выполнит все действия с данными D, значение переменной F(D) :нова устанавливается равным 1.
Блокирующие переменные могут использоваться не только при доступе к разделяемым данным, но и при доступе к разделяемым ресурсам любого вида.
Если все потоки написаны с учетом вышеописанных соглашений, то взаимное исключение гарантируется. При этом потоки могут быть прерваны операционной системой в любой момент и в любом месте, в том числе в критической секции.
Однако следует заметить, что одно ограничение на прерывания все же имеется. Нельзя прерывать поток между выполнением операций проверки и установки блокирующей переменной. Поясним это. Пусть в результате проверки переменной поток определил, что ресурс свободен, но сразу после того, не успев установить переменную в 0, был прерван. За время его приостановки другой поток занял ресурс, вошел в свою критическую секцию, но также был прерван, не завершив работы с разделяемым ресурсом. Когда управление было возвращено первому потоку, он, считая ресурс свободным, установил признак занятости и начал выполнять свою критическую секцию. Таким образом, был нарушен принцип взаимного исключения, что потенциально может привести к нежелательным последствиям. Во избежание таких ситуаций в системе команд многих компьютеров предусмотрена единая, неделимая команда анализа и присвоения значения логической переменной (например, команды ВТС, BTR и BTS процессора Pentium). При отсутствии такой команды в процессоре соответствующие действия должны реализовываться специальными системными примитивами, которые бы запрещали прерывания на протяжении всей операции проверки и установки.
Реализация взаимного исключения описанным выше способом имеет существенный недостаток: в течение времени, когда один поток находится в критической секции, другой поток, которому требуется тот же ресурс, получив доступ к процессору, будет непрерывно опрашивать блокирующую переменную, бесполезно тратя выделяемое ему процессорное время, которое могло бы быть использовано для выполнения какого-нибудь другого потока. Для устранения этого недостатка во многих ОС предусматриваются специальные системные вызовы для работы с критическими секциями.
На рис. 4ерационной системе Windows NT. Перед тем как начать изменение критических данных, поток выполняет системный вызов Enter-Critical Section О. В рамках этого вызова сначала выполняется, как и в предыдущем случае, проверка блокирующей переменной, отражающей состояние критического ресурса. Если системный вызов определил, что ресурс занят (FCD) — 0), он в отличие от предыдущего случая не выполняет циклический опрос, а переводит поток в состояние ожидания (D) и делает отметку о том, что данный поток должен быть активизирован, когда соответствующий ресурс освободится. Поток, который в это время использует данный ресурс, после выхода из критической секции должен выполнить системную функцию LeaveCriticalSectionQ, в результате чего блокирующая переменная принимает значение, соответствующее свободному состоянию ресурса (F(D) = 1), а операционная система просматривает очередь ожидающих этот ресурс потоков и переводит первый поток из очереди в состояние готовности.
Рис. 4.5.Реализация взаимного исключения с использованием системных функций входа в критическую секцию и выхода ив нее
Таким образом исключается непроизводительная потеря процессорного времени на циклическую проверку освобождения занятого ресурса. Однако в тех случаях, когда объем работы в критической секции небольшой и существует высокая вероятность в очень скором доступе к разделяемому ресурсу, более предпочтительным может оказаться использование блокирующих переменных. Действительно, в такой ситуации накладные расходы ОС по реализации функции входа в критическую секцию и выхода из нее могут превысить полученную экономию.
Обобщением блокирующих переменных являются так называемые семафоры Дийкстры. Вместо двоичных переменных Дийкстра (Dijkstra) предложил использовать переменные, которые могут принимать целые неотрицательные значения. Такие переменные, используемые для синхронизации вычислительных процессов, получили название семафоров.
Для работы с семафорами вводятся два примитива, традиционно обозначаемых Р и V. Пусть переменная S представляет собой семафор. Тогда действия V(S) и P(S) определяются следующим образом.
V(S): переменная S увеличивается на 1 единым действием. Выборка, наращивание и запоминание не могут быть прерваны. К переменной S нет доступа другим потокам во время выполнения этой операции.
P(S): уменьшение S на 1, если это возможно. Если 5=0 и невозможно уменьшить S, оставаясь в области целых неотрицательных значений, то в этом случае поток, вызывающий операцию Р, ждет, пока это уменьшение станет возможным. Успешная проверка и уменьшение также являются неделимой операцией.
Никакие прерывания во время выполнения примитивов, V и Р недопустимы.
В частном случае, когда семафор S может принимать только значения 0 и 1, он превращается в блокирующую переменную, которую по этой причине часто называют двоичным семафором. Операция Р заключает в себе потенциальную возможность перехода потока, который ее выполняет, в состояние ожидания, в то время как операция V может при некоторых обстоятельствах активизировать другой поток, приостановленный операцией Р.
Рассмотрим использование семафоров на классическом примере взаимодействия двух выполняющихся в режиме мультипрограммирования потоков, один из которых пишет данные в буферный пул, а другой считывает их из буферного пула. Пусть буферный пул состоит из N буферов, каждый из которых может содержать одну запись. В общем случае поток-писатель и поток-читатель могут иметь различные скорости и обращаться к буферному пулу с переменой интенсивностью. В один период скорость записи может превышать скорость чтения, в другой — наоборот. Для правильной совместной работы поток-писатель должен приостанавливаться, когда все буферы оказываются занятыми, и активизироваться при освобождении хотя бы одного буфера. Напротив, поток-читатель должен приостанавливаться, когда все буферы пусты, и активизироваться при появлении хотя бы одной записи.
Введем два семафора: е — число пустых буферов, и f — число заполненных буферов, причем в исходном состоянии е = N, a f = 0. Тогда работа потоков с общим буферным пулом может быть описана следующим образом .
Поток-писатель прежде всего выполняет операцию Р(е), с помощью которой он проверяет, имеются ли в буферном пуле незаполненные буферы. В соответствии с семантикой операции Р, если семафор е равен 0 (то есть свободных буферов в данный момент нет), то поток-писатель переходит в состояние ожидания. Если же значением е является положительное число, то он уменьшает число свободных буферов, записывает данные в очередной свободный буфер и после этого наращивает число занятых буферов операцией V(f). Поток-читатель действует аналогичным образом, с той разницей, что он начинает работу с проверки наличия заполненных буферов, а после чтения данных наращивает количество свободных буферов.
В данном случае предпочтительнее использовать семафоры вместо блокирующих переменных. Действительно, критическим ресурсом здесь является буферный пул, который может быть представлен как набор идентичных ресурсов — отдельных буферов, а значит, с буферным пулом могут работать сразу несколько потоков, и именно столько, сколько буферов в нем содержится. Использование двоичной переменной не позволяет организовать доступ к критическому ресурсу более чем одному потоку. Семафор же решает задачу синхронизации более гибко, допуская к разделяемому пулу ресурсов заданное количество потоков. Так, в нашем примере с буферным пулом могут работать максимум N потоков, часть из которых может быть «писателями», а часть — «читателями».
Таким образом, семафоры позволяют эффективно решать задачу синхронизации доступа к ресурсным пулам, таким, например, как набор идентичных в функциональном назначении внешних устройств (модемов, принтеров, портов), или Hav бор областей памяти одинаковой величины, или информационных структур. Во всех этих и подобных им случаях с помощью семафоров можно организовать доступ к разделяемым ресурсам сразу нескольких потоков.
Приведенный выше пример позволяет также проиллюстрировать еще одну проблему синхронизации — взаимные блокировки, называемые также дедлоками (deadlocks), клинчами (clinch), или тупиками. Покажем, что если переставить местами операции Р(е) и Р(b) в потоке-писателе, то при некотором стечении обстоятельств эти два потока могут взаимно блокировать друг друга.
Итак, пусть поток-писатель начинает свою работу с проверки доступности критической секции — операции Р(b), и пусть он первым войдет в критическую секцию. Выполняя операцию Р(е), он может обнаружить отсутствие свободных буферов и перейти в состояние ожидания. Как уже было показано, из этого состояния его может вывести только поток-читатель, который возьмет очередную запись из буфера. Но поток-читатель не сможет этого сделать, так как для этого ему потребуется войти в критическую секцию, вход в которую заблокирован потоком-писателем. Таким образом, ни один из этих потоков не может завершить начатую работу и возникнет тупиковая ситуация, которая не может разрешиться без внешнего воздействия.
Рассмотрим еще один пример тупика. Пусть двум потокам, принадлежащим разным процессам и выполняющимся в режиме мультипрограммирования, для выполнения их работы нужно два ресурса, например принтер и последовательный порт. Такая ситуация может возникнуть, например, во время работы приложения, задачей которого является распечатка информации, поступающей по модемной связи.
Поток А запрашивает сначала принтер, а затем порт, а поток В запрашивает устройства в обратном порядке. Предположим, что после того, как ОС назначила принтер потоку А и установила связанную с этим ресурсом блокирующую переменную, поток А был прерван. Управление получил поток В, который сначала выполнил запрос на получение СОМ-порта, затем при выполнении следующей команды был заблокирован, так как принтер оказался уже занятым потоком А. Управление снова получил поток А, который в соответствии со своей программой сделал попытку занять порт и был заблокирован, поскольку порт уже выделен потоку В. В таком положении потоки А и В могут находиться сколь угодно долго.
В зависимости от соотношения скоростей потоков они могут либо взаимно блокировать друг друга, либо образовывать очереди к разделяемым ресурсам, либо совершенно независимо использовать разделяемые ресурсы .
Тупиковые ситуации надо отличать от простых очередей, хотя те и другие возникают при совместном использовании ресурсов и внешне выглядят похоже: поток приостанавливается и ждет освобождения ресурса. Однако очередь — это нормальное явление, неотъемлемый признак высокого коэффициента использования ресурсов при случайном поступлении запросов. Очередь появляется тогда, когда ресурс недоступен в данный момент, но освободится через некоторое время, позволив потоку продолжить выполнение. Тупик же, что видно из его названия, является в некотором роде неразрешимой ситуацией. Необходимым условием возникновения тупика является потребность потока сразу в нескольких ресурсах..
В рассмотренных примерах тупик был образован двумя потоками, но взаимно блокировать друг друга может и большее число потоков.
Невозможность потоков завершить начатую работу из-за возникновения взаимных блокировок снижает производительность вычислительной системы. Поэтому проблеме предотвращения тупиков уделяется большое внимание. На тот случай, когда взаимная блокировка все же возникает, система должна предоставить администратору-оператору средства, с помощью которых он смог бы распознать тупик, отличить его от обычной блокировки из-за временной недоступности ресурсов. И наконец, если тупик диагностирован, то нужны средства для снятия взаимных блокировок и восстановления нормального вычислительного процесса. Тупики могут быть предотвращены на стадии написания программ, то есть программы должны быть написаны таким образом, чтобы тупик не мог возникнуть при любом соотношении взаимных скоростей потоков. Так, если бы в вышеразобранном примере поток А и поток В запрашивали ресурсы в одинаковой последовательности, то тупик был бы в принципе невозможен. Другой, более гибкий подход к предотвращению тупиков заключается в том, что ОС каждый раз при запуске задач анализирует их потребности в ресурсах и определяет, может ли в данной мультипрограммной смеси возникнуть тупик. Если да, то запуск новой задачи временно откладывается. ОС может также использовать определенные правила при назначении ресурсов потокам, например, ресурсы могут выделяться операционной системой в определенной последовательности, общей для всех потоков.
В тех же случаях, когда тупиковую ситуацию не удалось предотвратить, важно быстро и точно ее распознать, поскольку блокированные потоки не выполняют никакой полезной работы. Если тупиковая ситуация образована множеством потоков, занимающих массу ресурсов, распознавание тупика является нетривиальной задачей. Существуют формальные, программно-реализованные методы распознавания тупиков, основанные на ведении таблиц распределения ресурсов и таблиц запросов к занятым ресурсам. Анализ этих таблиц позволяет обнаружить взаимные блокировки.
Если же тупиковая ситуация возникла, то не обязательно снимать с выполнения все заблокированные потоки. Можно снять только часть из них, освободив ресурсы, ожидаемые остальными потоками, можно вернуть некоторые потоки в область подкачки, можно совершить «откат» некоторых потоков до так называемой контрольной точки, в которой запоминается вся информация, необходимая для восстановления выполнения программы с данного места. Контрольные точки расставляются в программе в тех местах, после которых возможно возникновение тупика.
Критическая секция (critical section) — это небольшой участок кода, требующий мо нопольного доступа к каким-то общим данным. Она позволяет сделать так, чтобы единовременно только один поток получал доступ к определенному ресурсу Есте ственно, система может в любой момент вытеснить Ваш поток и подключить к про цессорудругой, но ни один из потоков, которым нужен занятый Вами ресурс, не по лучит процессорное время до тех пор, пока Баш поток не выйдет за границы крити ческой секции.
Вот пример кода, который демонстрирует, что может произойти без критической секции:
const int MAX_TIMES = 1000,
int g_nIndex — 0,
DWORD WINAPI FirstThread(PVOID pvParam)
while (g_nIndex FirstThread, она заполнила бы массив g_dwTimes набором чисел с возраста ющими значениями. Это верно и в отношении SecondThread — если бы она тоже ис полнялась независимо. В идеале обе функции даже при одновременном выполнении должны бы по-
прежнему заполнять массив тем же набором чисел. Но в нашем коде возникает проблема: масив g_dwTimes не будет заполнен, как надо, потому что фун кции обоих потоков одновременно обращаются к одним и тем жс глобальным пере менным. Вот как это может произойти.
Допустим, мы только что начали исполнение обоих потоков в системе с одним процессором Первым включился в работу второй поток, т e функция SecondThread (что вполне вероятно), и только она успела увеличить счетчик g_nIndex 1, как си
стема вытеснила ее поток и перешла к исполнению FtrstThread Та заносит в g_dwTi mes[1] показания системного времени, и процессор вновь переключается на испол нение второго потока. SecondThread теперь присваивает элементу g_dwTtmes[1 — 1] новые показания системного времени Поскольку эта операция выполняется позже, новые показания, естественно, выше, чем записанные в элемент g_dwTimes[1]фyнк цией FirstThread Отметьте также, что сначала заполняется первый элемент массива и только потом нулевой. Таким образом, данные в массиве оказываются ошибочными.
Согласен, пример довольно надуманный, но, чтобы привести реалистичный, нуж но минимум несколько страниц кода, Важно другое теперь Вы легко представите, что может произойти в действительности Возьмем пример с управлением связанным списком объектов. Если доступ к связанному списку нс синхронизирован, один по ток может добавить элемент в список в тот момент, когда другой поток пытается найти в нсм какойто элемент. Ситуация станет еще более угрожающей, ссли оба потока одновременно добавят в список новые элементы. Так что, используя критические сек ции, можно и нужно координировать доступ потоков к структурам данных.
Теперь, когда Вы видите все «подводные камни», попробуем исправить этот фраг мент кода с помощью критической секции:
const int MAX_TIMES = 1000; int g_nIndex = 0;
DWORD g_dwTimes[MAX_TIMES]; CRITICAL_SECTION g_cs;
DWORD WINAPI FirstThread(PVOID pvParam)
for (BOOL fContinue = TRUE; fContinue; )
EnterCriticalSection(&g_cs); if (g_nIndex g_cs, а потом «обер нул" весь код, работающий с разделяемым ресурсом (в нашем примере это строки с g_nIndex и g_dwTimes), вызовами EnterCriticalSection и LeaveCriticalSection. Заметьте, что при вызовах этих функций я передаю адрес g_cs.
Запомните несколько важных вещей. Если у Вас есть ресурс, разделяемый несколь кими потоками, Вы должны создать экземпляр структуры CRITICAL_SECTION. Так как я пишу эти строки в самолете, позвольте провести следующую аналогию. Структура CRITICAL_SECTION похожа на туалетную кабинку в самолете, а данные, которые нуж но защитить, — на унитаз, Туалетная кабинка (критическая секция) в самолете очень маленькая, и единовременно в ней может находиться только один человек (поток), пользующийся унитазом (защищенным ресурсом)
Если у Вас есть ресурсы, всегда используемые вместе, Вы можете поместить их в одну кабинку — единственная структура CRITICAL_SECTION будет охранять их всех. Но если ресурсы не всегда используются вместе (например, потоки 1 и 2 работают с одним ресурсом, а потоки 1 и 3 — с другим), Вам придется создать им по отдельной кабинке, или структуре CRITICAL_SECTION.
Теперь в каждом участке кода, где Вы обращаетесь к разделяемому ресурсу, вы зывайте EnterCriticaSection, передавая ей адрес структуры CRITICAL_SECTION, кото рая выделена для этого ресурса. Иными словами, поток, желая обратиться к ресурсу, должен сначала убедиться, нет ли на двери кабинки знака «занято». Структура CRITI CAL_SECTION идентифицирует кабинку, в которую хочет войти поток, а функция EnterCriticalSection — тот инструмент, с помощью которого он узнает, свободна или занята кабинка. EnterCriticalSection допустит вызвавший ее поток в кабинку, если оп ределит, что та свободна. В ином случае (кабинка занята) EnterCriticalSection заставит его ждать, пока она не освободится.
Поток, покидая участок кода, где он работал с защищенным ресурсом, должен вызвать функцию LeaveCriticalSection. Тем самым он уведомляет систему о том, что кабинка с данным ресурсом освободилась. Если Вы забудете это сделать, система бу дет считать, что ресурс все еще занят, и не позволит обратиться к нему другим жду щим потокам, То есть Вы вышли из кабинки и оставили на двери знак "занято".
Самое сложное — запомнить, что любой участок кода, работающего с разде ляемым ресурсом, нужно заключить в вызовы функций EnterCrtticalSection и LeaveCriticalSection. Если Вы забудете сделать это хотя бы в одном месте, ре сурс может быть поврежден Так, если в FirstThread убрать вызовы EnterCritical Section и LeaveCriticalSection, содержимое переменных g_nIndex и g_dwTimes станет некорректным — даже несмотря на то что в S econdThread функции
EnterCriticalSection и LeaveCriticalSection вызываются правильно.
Забыв вызвать эти функции, Вы уподобитесь человеку, который рвется в туалетную кабинку, не обращая внимания па то, есть в ней кто-нибудь или нет. Поток пробивает себе путь к ресурсу и берется им манипулировать. Как Вы прекрасно понимаете, стоит лить одному потоку проявить такую "грубость", и Ваш ресурс станет кучкой бесполезных байтов.
Применяйте критические секции, если Вам не удается решить проблему синхро низации зз счет Interlocked-функций. Преимущество критических секций в том, что они просты в использовании и выполняются очень быстро, так как реализованы на основе Interlocked — функций А главный недостаток — нельзя синхронизировать потоки в разных процессах. Однако в главе 10 я продемонстрирую Вам свой синхронизиру ющий объект, который я назвал оптексом. На его примере Вы увидите, как реализу ются критические секции на уровне операционной системы и как этот объект рабо тает с потоками разных процессов.
Критические секции: важное дополнение
Теперь, когда у Вяс появилось общее представление о критических секциях (зачем они нужны и как с их помощью можно монопольно распоряжаться разделяемым ресур сом), давайте повнимательнее приглядимся ктому, как они устроены Начнем со струк туры CRITICAL_SECTION. Вы не найдете ее в Platform SDK — о ней нет даже упомина ния. В чсм дело?
Хотя CRITICAL_SECTION не относится к недокументированным структурам, Micro soft полагает, что Вам незачем знать, как она устроена И это правильно. Для нас она нвляется своего рода черным ящиком — сама структура известна, а ее элементы — нет. Конечно, поскольку CRITICAL_SECTION — не более чем одна из структур, мы можем сказать, из чего она состоит, изучив заголовочные файлы. (CRITICAT,_SECTlON опреде лена в файле WinNT.h как RTL_CRITICAL_SECTION, а тип структуры RTL_CRITICAL_SEC TION определен в файле WinBase.h,) Но никогда не пишите код, прямо ссылающийся на ее элементы.
Вы работаете со структурой CRITICAL_SECTION исключительно через функции Windows, передавая им адрес соответствующего экземпляра этой структуры Функции сами знают, как обращаться с ее элементами, и гарантируют, что она всегда будет в согласованном состоянии. Так что теперь мы перейдем к рассмотрению этих функций.
Обычно структуры CRITICAL_SECTION создаются как глобальные переменные, доступные всем потокам процесса. Но ничто не мешает нам создавать их как локаль ные переменные или переменные, динамически размещаемые в куче, Есть только два условия, которые надо соблюдать Во-первых, все потоки, которым может понадобить ся ресурс, должны знать адрес структуры CRITICAL_SECTION, которая защищает этот ресурс. Вы можете получить ее адрес, используя любой из существующих механиз мов. Во-вторых, элементы структуры CRITICAL_SECTION следует инициализировать до обращения какого-либо потока к защищенному ресурсу. Структура инициализи руется ВЫЗОВОМ:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
Эта функция инициализирует элементы структуры CRITICAL_SECTION, на которую указывает параметр pcs. Поскольку вся работа данной функции заключается в иници ализации нескольких переменных-членов, она не дает сбоев и поэтому ничего не возвращает (void). InitializeCriticalSection должна быть вызвана до того, как один иэ потоков обратится к EnterCriticalSection. В документации Platform SDK недвусмыслен но сказано, что попытка воспользоваться неинициализированной критической сек цией даст непредсказуемые результаты.
Если Вы знаете, что структура CRITICAL_SECTION больше не понадобится ни од- . ному потоку, удалите ее, вызвав DeleteCriticalSection:
VOID DeleteCriticalSection(PCRITICAL__SECTION pcs);
Она сбрасывает все переменные-члены внутри этой структуры. Естественно, нельзя удалять критическую секцию в тот момент, когда ею все еще пользуется ка кой-либо поток. Об этом нас предупреждают и в документации Platform SDK.
Участок кода, работающий с разделяемым ресурсом, предваряется вызовом:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
Первое, что делает EnterCriticalSection, — исследует значения элементов структу ры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. EnterCriticalSection выполняет следующие действия.
Если ресурс свободен, EnterCriticalSection модифицирует элементы структуры, указывая, что вызывающий поток занимает ресурс, после чего немедленно возвращает управление, и поток продолжает свою работу (получив доступ к ресурсу).
Если значения элементов структуры свидетельствуют, что ресурс уже захвачен вызывающим потоком, EnterCriticalSection обновляет их, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и немедленно возвращает уп равление. Такая ситуация бывает нечасто — лишь тогда, когда поток два раза подряд вызывает EnterCriticalSection без промежуточного вызова LeaweCritical
Если значения элементов структуры указывают на то, что ресурс занял другим потоком, EnterCriticalSection переводит вызывающий поток в режим ожидания. Это потрясающее свойство критических секций: поток, пребывая в ожидании, не тратит ни кванта процессорного времени Система запоминает, что данный поток хочет получить доступ к ресурсу, и — как только поток, занимавший этот ресурс, вызывает LeaveCriticalSection — вновь начинает выделять нашему пото ку процессорное время При этом она передает ему ресурс, автоматически обновляя элементы структуры CRITICAL_SECTION
Внутреннее устройство EnterCriticalSection не слишком сложно; она выполняет лишь несколько простых операций. Чем она действительно ценна, так это способно стью выполнять их на уровне атомарного доступа. Даже если два потока на много процессорной машине одновременно вызовут EnterCriticalSection, функция все равно корректно справится со своей задачей: один пошк получит ресурс, другой — перей дет в ожидание.
Поток, переведенный EnterCriticalSection в ожидание, может надолго лишиться доступа к процессору, а в плохо написанной программе — даже вообще не получить его. Когда именно так и происходит, говорят, что поток "голодает".
В действительности потоки, ожидающие освобождения критической секции, никогда не блокируются «навечно» EnterCriticalSection устроена так, что по истечении определенного времени, генерирует исключение. После этого Вы можете подключить к своей программе отладчик и посмотреть, что в ней слу чилось. Длительность времени ожидания функцией EnterCriticaiSection опреде ляется значением параметра CriticalSectionTimeout, который хранится в следу ющем разделе системного реестра:
HKEY_LOCAL_MACHlNESystemCurrentControlSetControlS ession Manager
Длительность времени ожидания измеряется в секундах и по умолчанию равна 2 592 000 сскунд (что составляет ровно 30 суток). Не устанавливайте слишком малое значение этого параметра (например, менее 3 секунд), так как иначе Вы нарушиге работу других потоков и приложений, которые обычно ждут освобождения критической секции дольше трех секунд.
Вместо EnterCriticalSection Вы можете воспользоваться;
BOOL TryEnterCriticalSection(PCRITICAL_SECTIQN pcs);
Эта функция никогда не приостанавливает выполнение вызывающего потока. Но возвращаемое ею значение сообщает, получил ли этот поток доступ к ресурсу. Если при ее вызове указанный ресурс занят другим потоком, она возвращает FALSE.
TryEnterCriticalSection позволяет потоку быстро проверить, доступен ли ресурс, и ссли нет, папяться чем-нибудь другим. Если функция возвращает TRUE, значит, она обновила элементы структуры CRITICAL_SECTION так, чтобы они сообщали о захва те ресурса вызывающим потоком. Отсюда следует, что для каждого вызова функции TryEnterCriticalScction, где она возвращает TRUE, надо предусмотреть парный вызов
В Windows 98 функция TryEnterCriticalSection определена, но не реализована. При
ее вызове всегда возвращается FALSE.
В конце участка кода, использующего разделяемый ресурс, должен присутствовать вызов.
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
Эта функция просматривает элементы структуры CRITICAL_SECTION и уменьша ет счетчик числа захватов ресурса вызывающим потоком на 1. Если его значение боль ше 0, LeaveCriticalSection ничего не делает и просто возвращает управление.
Если значение счетчика достигло 0, LeaveCnitcalSection сначала выясняет, есть ли в системе другие потоки, ждущие данный ресурс в вызове EnlerCriticalSection Если есть хотя бы один такой поток, функция настраивает значения элементов структуры, что бы они сигнализировали о занятости ресурса, и отдает его одному из ждущих пото ков (поток выбирается «по справедливости») Если же ресурс никому не нужен, Leave CriticalSection соответственно сбрасывает элементы структуры.